﻿namespace FasterThanLightEngine.Math
{
    using System;

    /// <summary>
    /// A class reprisenting a 3D vector and encapsulating related operations.
    /// </summary>
    public class Vector3D
    {
        /// <summary>
        /// An enum used to describe the how the axes of the <see cref="Vector3D"/>
        /// map to a <see cref="Vector2D"/>.
        /// </summary>
        public enum VectorDemotionType
        {
            XtoXYtoY,
            XtoXZtoY
        }

        /// <summary>
        /// Gets or sets the X component of the <see cref="Vector3D"/>.
        /// </summary>
        public double X
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the Y component of the <see cref="Vector3D"/>.
        /// </summary>
        public double Y
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the Z component of the <see cref="Vector3D"/>.
        /// </summary>
        public double Z
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the magnitude of the <see cref="Vector3D"/>.
        /// </summary>
        public double Magnitude
        {
            get
            {
                return Math.Sqrt(this.Magnitude2);
            }
        }

        /// <summary>
        /// Gets the square of the magnitude of the <see cref="Vector3D"/>.
        /// This is preferred as it avoids the overhead of calculating the
        /// Square Root of the magnitude. Square the value you are comparing
        /// against for comparison checks.
        /// </summary>
        public double Magnitude2
        {
            get
            {
                return this.X*this.X + this.Y*this.Y + this.Z*this.Z;
            }
        }

        /// <summary>
        /// Creates a default instance of a <see cref="Vector3D"/>.
        /// </summary>
        public Vector3D()
        { }

        /// <summary>
        /// Creates a new instance of the <see cref="Vector3D"/> with the given values.
        /// </summary>
        /// <param name="x">The X component of the <see cref="Vector3D"/>.</param>
        /// <param name="y">The Y component of the <see cref="Vector3D"/>.</param>
        /// <param name="z">The Z component of the <see cref="Vector3D"/>.</param>
        public Vector3D(double x, double y, double z)
        {
            this.X = x;
            this.Y = y;
            this.Z = z;
        }

        /// <summary>
        /// Creates a new instance of the <see cref="Vector3D"/> class based off a current one.
        /// </summary>
        /// <param name="other">The current <see cref="Vector3D"/> that the new <see cref="Vector3D"/> will be based on.</param>
        public Vector3D(Vector3D other)
        {
            if (other != null)
            {
                this.X = other.X;
                this.Y = other.Y;
                this.Z = other.Z;
            }
        }

        /// <summary>
        /// Calculates the Dot Product of the current <see cref="Vector3D"/> and another <see cref="Vector3D"/>.
        /// </summary>
        /// <param name="other">The right operand for the calculation.</param>
        /// <returns>The scalar result of the Dot Product.</returns>
        public double DotProduct(Vector3D other)
        {
            if (other == null)
                throw new ArgumentException("Vector parameter for DotProduct must not be null.");

            return this.X * other.X + this.Y * other.Y + this.Z * other.Z;
        }

        /// <summary>
        /// Promotes a <see cref="Vector3D"/> into a <see cref="Vector2D"/> using the given mapping between the axes.
        /// </summary>
        /// <param name="conversionType">The mapping between source and destination axes.</param>
        /// <returns>A <see cref="Vector2D"/>.</returns>
        public Vector2D ToVector2D(VectorDemotionType conversionType = VectorDemotionType.XtoXYtoY)
        {
            var result = new Vector2D();

            switch (conversionType)
            {
                case VectorDemotionType.XtoXYtoY:
                    result = new Vector2D(this.X, this.Y);
                    break;
                case VectorDemotionType.XtoXZtoY:
                    result = new Vector2D(this.X, this.Z);
                    break;
            }

            return result;
        }

        /// <summary>
        /// Provides a standard string reprisentation of the <see cref="Vector3D"/>.
        /// </summary>
        /// <returns>The string reprisentation of the <see cref="Vector3D"/>.</returns>
        public override string ToString()
        {
            return "(" + this.X + ", " + this.Y + ", " + this.Z + ")";
        }

        /// <summary>
        /// Adds the given <see cref="Vector3D"/> to another <see cref="Vector3D"/>.
        /// </summary>
        /// <param name="left">The left operand.</param>
        /// <param name="right">The right operand.</param>
        /// <returns>The resultant <see cref="Vector3D"/>.</returns>
        public static Vector3D operator +(Vector3D left, Vector3D right)
        {
            if (left == null || right == null)
                throw new ArgumentException("Neither operator for Vector-Vector addition can be null.");

            return new Vector3D(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
        }

        /// <summary>
        /// Subtracts the given <see cref="Vector3D"/> from another <see cref="Vector3D"/>.
        /// </summary>
        /// <param name="left">The left operand.</param>
        /// <param name="right">The right operand.</param>
        /// <returns>The resultant <see cref="Vector3D"/>.</returns>
        public static Vector3D operator -(Vector3D left, Vector3D right)
        {
            if (left == null || right == null)
                throw new ArgumentException("Neither operator for Vector-Vector subtraction can be null.");

            return new Vector3D(left.X - right.X, left.Y - right.Y, left.Z - right.Z);
        }

        /// <summary>
        /// Multiplies the given <see cref="Vector3D"/> by a scalar integer.
        /// </summary>
        /// <param name="left">The <see cref="Vector3D"/>.</param>
        /// <param name="right">The multiplier.</param>
        /// <returns>The result of the multiplication.</returns>
        public static Vector3D operator *(Vector3D left, int right)
        {
            if (left == null)
                throw new ArgumentException("Left operand for Vector multiplication must not be null.");

            return new Vector3D(left.X * right, left.Y * right, left.Z * right);
        }

        /// <summary>
        /// Multiplies the given <see cref="Vector3D"/> by a scalar integer.
        /// </summary>
        /// <param name="left">The multiplier.</param>
        /// <param name="right">The <see cref="Vector3D"/>.</param>
        /// <returns>The result of the multiplication.</returns>
        public static Vector3D operator *(int left, Vector3D right)
        {
            if (right == null)
                throw new ArgumentException("Right operand for Vector multiplication must not be null.");

            return new Vector3D(left * right.X, left * right.Y, left * right.Z);
        }

        /// <summary>
        /// Multiplies the two given <see cref="Vector3D"/> values together.
        /// </summary>
        /// <param name="left">The left operand.</param>
        /// <param name="right">The right operand.</param>
        /// <returns>The result of the multiplication.</returns>
        public static Vector3D operator *(Vector3D left, Vector3D right)
        {
            if (left == null || right == null)
                throw new ArgumentException("Neither operator for Vector-Vector multiplication can be null.");

            return new Vector3D(left.X * right.X, left.Y * right.Y, left.Z * right.Z);
        }

        /// <summary>
        /// Divides the given <see cref="Vector3D"/> with a scalar value.
        /// </summary>
        /// <param name="left">The <see cref="Vector3D"/> to divide.</param>
        /// <param name="right">The scalar value to divide by.</param>
        /// <returns>The result of the division.</returns>
        public static Vector3D operator /(Vector3D left, int right)
        {
            if (left == null)
                throw new ArgumentException("Left operand for Vector division must not be null.");

            return new Vector3D(left.X / right, left.Y / right, left.Z / right);
        }

        // TODO: Add addition and subtraction operators for points. This is the primary way points should be advanced in the engine.
    }
}
